package com.dycong.common.network;

import com.dycong.common.common.ApplicationConfig;
import lombok.extern.log4j.Log4j;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.HttpClientUtils;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static java.util.stream.Collectors.*;

/**
 * proxy
 * // todo cookie
 * // todo user agent
 * <p>
 * Created by yu on 16-3-24.
 */
@Log4j
public class RichStateHttpClientImpl implements RichStateHttpClient {

    public static final int DEFAULT_MAX_TOTAL = 100;
    public static final int DEFAULT_MAX_PER_ROUTE = 10;

    RequestConfig proxyConfig;

    HttpClient client;

    /**
     * @param proxyHost
     * @param proxyPort
     */
    RichStateHttpClientImpl(String proxyHost, int proxyPort) {
        setProxyConfigInOrder(proxyHost, proxyPort);


        int maxTotal = ApplicationConfig.getInt("maxTotal").orElse(DEFAULT_MAX_TOTAL);
        int maxPerRoute = ApplicationConfig.getInt("maxPerRoute").orElse(DEFAULT_MAX_PER_ROUTE);

        log.info("maxTotal: " + maxTotal + ", maxPerRoute: " + maxPerRoute);

        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
        // max connections
        cm.setMaxTotal(maxTotal);
        // max connections per route
        cm.setDefaultMaxPerRoute(maxPerRoute);
        // set max connections for a specified route

        client = HttpClients.custom()
                .setConnectionManager(cm)
                .build();
    }

    final static Map<String, RichStateHttpClientImpl> instanceMap = new HashMap<>();

    public static RichStateHttpClientImpl getInstance() {
        return getInstance("", 0);
    }

    public static RichStateHttpClientImpl getInstance(String proxyHost, int proxyPort) {
        String key = proxyHost + proxyPort;
        synchronized ((RichStateHttpClientImpl.class + key).intern()) {
            if (proxyHost == null || proxyHost.equals("") || proxyPort == 0) {
                key = "";
            }
            RichStateHttpClientImpl client = instanceMap.get(key);
            if (client == null) {
                client = new RichStateHttpClientImpl(proxyHost, proxyPort);
                instanceMap.put(key, client);
            }
            return client;
        }
    }

    Result execute(HttpRequestBase request, String encoding) {
        try {
            if (proxyConfig != null)
                request.setConfig(proxyConfig);
            HttpResponse response = client.execute(request);
            try {
                Map<String, List<String>> responseMap = Arrays.stream(response.getAllHeaders())
                        .collect(groupingBy(
                                h -> h.getName(),
                                mapping(h -> h.getValue(), toList())
                        ));
                String content = EntityUtils.toString(response.getEntity(), encoding);
                Result result = new Result();
                result.setResponseHeader(responseMap);
                result.setStatusCode(response.getStatusLine().getStatusCode());
                result.setContent(content);
                log.debug("http call result: " + result);
                return result;
            } finally {
                HttpClientUtils.closeQuietly(response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }


    @Override
    public Result doGet(String url, String encoding, Map<String, String> header, Map<String, String> body) {
        url = formatGetUrl(url, body, encoding);
        HttpGet get = new HttpGet(url);
        log.debug("do GET: " + url);

        if (header != null)
            header.forEach(get::setHeader);

        return execute(get, encoding);
    }


    @Override
    public Result doPost(String url, String encoding, Map<String, String> headerMap, Map<String, String> paramMap) {
        HttpPost post = new HttpPost(url);
        if (headerMap == null) {
            headerMap = new HashMap<>();
        }
        headerMap.forEach(post::addHeader);
        if (!headerMap.containsKey("Content-type"))
            post.addHeader("Content-type", "application/x-www-form-urlencoded;charset=" + encoding);

        if (paramMap != null) {
            List<NameValuePair> params = paramMap.keySet()
                    .stream()
                    .map(key -> new BasicNameValuePair(key, paramMap.get(key)))
                    .collect(Collectors.toList());
            try {
                HttpEntity entity = new UrlEncodedFormEntity(params, encoding);
                post.setEntity(entity);
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException(e);
            }
        }

        return execute(post, encoding);
    }

    @Override
    public Result postJson(String url, String encoding, Map<String, String> headerMap, String json) {
        HttpPost post = new HttpPost(url);
        if (headerMap != null) {
            headerMap.forEach(post::setHeader);
        }

        post.setEntity(new StringEntity(json, encoding));
        post.addHeader("content-type", "application/json; charset=" + encoding);

        return execute(post, encoding);
    }


    /**
     * proxy order:
     * 1: pass as parameter
     * 2: config in ApplicationConfig
     * 3: System config, e.g. JAVA_OPTS
     *
     * @param proxyHost
     * @param proxyPort
     */
    void setProxyConfigInOrder(String proxyHost, int proxyPort) {
        if (setProxyConfig(proxyHost, proxyPort))
            return;

        log.info("try to set proxy from ApplicationConfig");
        String configHost = ApplicationConfig.get("proxyHost").orElse(null);
        int configPort = ApplicationConfig.getInt("proxyPort").orElse(0);
        if (setProxyConfig(configHost, configPort))
            return;

        log.info("try to set proxy from environment variable");
        String host = System.getProperty("http.proxyHost");
        try {
            int port = Integer.valueOf(System.getProperty("http.proxyPort"));
            setProxyConfig(host, port);
        } catch (RuntimeException e) {
            log.info("try to get proxy config from ENV failed. no proxy now");
        }
    }

    boolean setProxyConfig(String host, int port) {
        if (isProxyAvailable(host, port)) {
            log.info("setting proxy config: " + host + ":" + port);
            HttpHost proxy = new HttpHost(host, port);
            this.proxyConfig = RequestConfig.custom().setProxy(proxy).build();
            return true;
        } else {
            log.debug("proxy is not available: " + host + ":" + port);
            return false;
        }
    }

    boolean isProxyAvailable(String host, int port) {
        //TODO
        return host != null && !host.equals("") && port > 0 && port < (1 << 16);
    }


    public String formatGetUrl(String originalURl, Map<String, String> paramMap, String encoding) {
        if (paramMap == null)
            return originalURl;
        List<? extends NameValuePair> list =
                paramMap.entrySet().stream().map(entry -> new BasicNameValuePair(entry.getKey(), entry.getValue())).collect(toList());
        if (originalURl.contains("?")) {
            return originalURl + "&" + URLEncodedUtils.format(list, encoding);
        } else {
            return originalURl + "?" + URLEncodedUtils.format(list, encoding);
        }
    }

}
